The biggest change this version was the addition of a Loan entity. A loan represents information about the relationship between Book and Patron. It specifically stores the checkout date and the due date. The Loan entity represents a so-called join table in the database. There are ways to specify a join table without creating an Entity, however we created the entity because we wanted to store additional information about the relationship. It also, arguably, makes some of our queries easier having Loan as an entity rather that just described as a join table.
Loan.java
packageentity;importjava.util.Date;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.Id;importjavax.persistence.IdClass;importjavax.persistence.JoinColumn;importjavax.persistence.ManyToOne;importjavax.persistence.NamedQueries;importjavax.persistence.NamedQuery;importjavax.persistence.Temporal;importjavax.persistence.TemporalType;/**
* I'm an entity with a 2-part key. The first part of my key is a Book id, the
* second is a Patron id.
* <P>
* To make this work, we must do several things: Use the annotation IdClass,
* Specify each of the parts of the id by using Id annotation (2 times in this
* case), Set each Id column to both insertable = false and updatable = false,
* Create an attribute representing the type of the id (Book, Patron), Use
* JoinColumn with the name = id and insertable = false, updatable = false.
*/
@Entity
@IdClass(LoanId.class)
@NamedQueries({
@NamedQuery(name = "Loan.booksLoanedTo",
query = "SELECT l.book FROM Loan l WHERE l.patron.id = :patronId"),
@NamedQuery(name = "Loan.byBookId",
query = "SELECT l FROM Loan l WHERE l.bookId = :bookId"),
@NamedQuery(name = "Loan.overdueBooks",
query = "SELECT l.book FROM Loan l WHERE l.dueDate < :date"),
@NamedQuery(name = "Loan.patronsWithOverdueBooks",
query = "SELECT l.patron FROM Loan l WHERE l.dueDate < :date")})publicclass Loan {/**
* Part 1 of a 2-part Key. The name must match the name in LoanId.
*/
@Id
@Column(name = "bookId", insertable = false, updatable = false)privateLong bookId;/**
* Part 2 of a 2-part Key. The name must match the name in LoanId.
*/
@Id
@Column(name = "patronId", insertable = false, updatable = false)privateLong patronId;/**
* A duplicate column in a sense, this one gives us the actual Patron rather
* than just having the id of the Patron.
*
* In the reference material I read, putting in insertable and updatable =
* false did not seem required. However, when using the hibernate entity
* manger I got a null pointer exception and had to step through the source
* code to fix the problem.
*/
@ManyToOne
@JoinColumn(name = "patronId", insertable = false, updatable = false)private Patron patron;/**
* Same comment as for patron attribute above plus...
*
* It seems this should be a OneToOne relationship but doing so will give
* you an obscure exception with little explanation as to why.
*/
@ManyToOne
@JoinColumn(name = "bookId", insertable = false, updatable = false)privateBook book;/**
* The date type can represent a date, a time or a time stamp (date and
* time). In our case we just want the date.
*/
@Temporal(TemporalType.DATE)
@Column(updatable = false)privateDate checkoutDate;
@Temporal(TemporalType.DATE)
@Column(updatable = false)privateDate dueDate;public Loan(){}public Loan(finalBook b, final Patron p, finalDate checkoutDate){
setBookId(b.getId());
setPatronId(p.getId());
setBook(b);
setPatron(p);
setCheckoutDate(checkoutDate);
setDueDate(b.calculateDueDateFrom(checkoutDate));}publicBook getBook(){return book;}publicvoid setBook(finalBook book){this.book = book;}publicLong getBookId(){return bookId;}publicvoid setBookId(finalLong bookId){this.bookId = bookId;}publicDate getCheckoutDate(){return checkoutDate;}publicvoid setCheckoutDate(finalDate checkoutDate){this.checkoutDate = checkoutDate;}publicDate getDueDate(){return dueDate;}publicvoid setDueDate(finalDate dueDate){this.dueDate = dueDate;}public Patron getPatron(){return patron;}publicvoid setPatron(final Patron patron){this.patron = patron;}publicLong getPatronId(){return patronId;}publicvoid setPatronId(finalLong patronId){this.patronId = patronId;}publicvoid checkin(finalDate checkinDate){
getBook().checkin(checkinDate);
getPatron().checkin(this);}}
LoanId.java
packageentity;importjava.io.Serializable;/**
* I'm a custom, multi-part key. I represent the key of a Loan object, which
* consists of a Patron id and a Book id.
*
* These two values together must be unique. The names of my attributes are the
* same as the names used in Loan (patronId, bookId).
*
* I also must be Serializable.
*/publicclass LoanId implementsSerializable{privatestaticfinallong serialVersionUID = -6272344103273093529L;/**
* The following two fields must have names that match the names used in the
* Loan class.
*/privateLong patronId;privateLong bookId;public LoanId(){}public LoanId(finalLong patronId, finalLong bookId){this.patronId = patronId;this.bookId = bookId;}publicLong getBookId(){return bookId;}publicLong getPatronId(){return patronId;}
@Overridepublicboolean equals(finalObject rhs){return rhs instanceof LoanId &&((LoanId) rhs).bookId.equals(bookId)&&((LoanId) rhs).patronId.equals(patronId);}
@Overridepublicint hashCode(){return patronId.hashCode()* bookId.hashCode();}}
Book.java
packageentity;importjava.util.Calendar;importjava.util.Date;importjava.util.HashSet;importjava.util.Set;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.ManyToMany;importjavax.persistence.NamedQueries;importjavax.persistence.NamedQuery;importjavax.persistence.OneToOne;importutil.DateTimeUtil;/**
* I represent a Book. I have one named query to find a book by its isbn number.
* I have a many to many relationship with author. Since I define the mappedBy,
* I'm the (arbitrarily picked) master of the relationship. I also take care of
* cascading changes to the database. I also have One To One relationship with a
* Loan that is optional (so it can be null). If I have a loan, I'm checked out
* and the loan knows the Patron, checkout date and due date.
*/
@Entity/**
* A named query must have a globally unique name. That is why these are named
* "Book."... These queries could be associated with any entity. Given that this
* query deals with books, it seems appropriate to put it here. Named queries
* will probably be pre-compiled. They are available from the entity manager by
* using em.getNamedQueyr("Book.findById").
*/
@NamedQueries({ @NamedQuery(name = "Book.findByIsbn",
query = "SELECT b FROM Book b WHERE b.isbn = :isbn")})publicclassBook{
@Id
@GeneratedValue
privateLong id;
@Column(length = 100, nullable = false)privateString title;
@Column(length = 20, nullable = false)privateString isbn;privateDate printDate;/**
* Authors may have written several books and vice-versa. We had to pick one
* side of this relationship as the primary one and we picked books. It was
* arbitrary but since we're dealing with books, we decided to make this
* side the primary size. The mappedBy connects this relationship to the one
* that is in Author. When we merge or persist, changes to this collection
* and the contents of the collection will be updated. That is, if we update
* the name of the author in the set, when we persist the book, the author
* will also get updated.
*
* Note that if we did not have the cascade setting here, they if we tried
* to persist a book with an unmanaged author (e.g. a newly created one),
* the entity manager would contain of a transient object.
*/
@ManyToMany(mappedBy = "booksWritten", cascade = { CascadeType.PERSIST,
CascadeType.MERGE})privateSet<Author> authors;/**
* Now, instead of directly knowing the patron who has borrowed me as in the
* previous version, I now hold on to a Loan object. The loan tracks both
* me, the patron as well as the checkout and due dates.
*/
@OneToOne(mappedBy = "book", cascade = CascadeType.PERSIST, optional = true)private Loan loan;publicBook(finalString t, finalString i, finalDate printDate,
final Author... authors){
setTitle(t);
setIsbn(i);
setPrintDate(printDate);for(Author a : authors){
addAuthor(a);}}publicBook(){}publicSet<Author> getAuthors(){if(authors == null){
authors = newHashSet<Author>();}return authors;}publicvoid setAuthors(finalSet<Author> authors){this.authors = authors;}publicLong getId(){return id;}publicvoid setId(finalLong id){this.id = id;}publicString getIsbn(){return isbn;}publicvoid setIsbn(finalString isbn){this.isbn = isbn;}publicDate getPrintDate(){return printDate;}publicvoid setPrintDate(finalDate printDate){this.printDate = printDate;}publicString getTitle(){return title;}publicvoid setTitle(finalString title){this.title = title;}publicvoid addAuthor(final Author author){
getAuthors().add(author);}public Loan getLoan(){return loan;}publicvoid setLoan(Loan loan){this.loan = loan;}
@Overridepublicboolean equals(finalObject rhs){return rhs instanceofBook&&((Book) rhs).getIsbn().equals(getIsbn());}
@Overridepublicint hashCode(){return getIsbn().hashCode();}publicboolean wasWrittenBy(Author a){return getAuthors().contains(a);}publicDate calculateDueDateFrom(Date checkoutDate){finalCalendar c = Calendar.getInstance();
c.setTime(checkoutDate);
c.add(Calendar.DATE, 14);return c.getTime();}publicboolean isOnLoanTo(final Patron p){return(getLoan() == null&& p == null) || getLoan()!= null&& getLoan().getPatron().equals(p);}publicboolean isCheckedOut(){return getLoan()!= null;}publicboolean dueDateEquals(finalDate date){return(date == null&&!isCheckedOut())
|| getLoan().getDueDate().equals(date);}publicDate getDueDate(){if(isCheckedOut()){return getLoan().getDueDate();}returnnull;}publicvoid checkin(Date checkinDate){finalDate dueDate = getDueDate();if(getLoan().getDueDate().before(checkinDate)){finaldouble amount = .25 * DateTimeUtil.daysBetween(dueDate,
checkinDate);final Fine f = new Fine(amount, checkinDate, this);
getLoan().getPatron().addFine(f);}
setLoan(null);}}
Fine.java
packageentity;importjava.util.Date;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToOne;importjavax.persistence.Temporal;importjavax.persistence.TemporalType;/**
* I represent a single fine assigned to a Patron for a book returned after its
* due date.
*
* I use new new features of JPA.
*/
@Entitypublicclass Fine {
@Id
@GeneratedValue
privateLong id;privatedouble amount;
@Temporal(TemporalType.DATE)privateDate dateAdded;
@OneToOne
privateBook book;public Fine(){}public Fine(finaldouble amount, finalDate dateAdded, finalBook book){
setAmount(amount);
setDateAdded(dateAdded);
setBook(book);}publicBook getBook(){return book;}publicvoid setBook(Book book){this.book = book;}publicDate getDateAdded(){return dateAdded;}publicvoid setDateAdded(Date dateAdded){this.dateAdded = dateAdded;}publicdouble getAmount(){return amount;}publicvoid setAmount(double fine){this.amount = fine;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}}
Patron.java
packageentity;importjava.util.ArrayList;importjava.util.Date;importjava.util.List;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Embedded;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToMany;importjavax.persistence.OneToOne;importjavax.persistence.OrderBy;importexception.InsufficientFunds;
@Entitypublicclass Patron {
@Id
@GeneratedValue
privateLong id;
@Embedded
privateName name;
@Column(length = 11, nullable = false)privateString phoneNumber;/**
* This next field refers to an object that is stored in another table. All
* updates are cascaded. So if you persist me, my address, which is in
* another table, will be persisted automatically. Updates and removes are
* also cascaded automatically.
*
* Note that cascading removes is a bit dangerous. In this case I know that
* the address is owned by only one Patron. In general you need to be
* careful automatically removing objects in related tables due to possible
* constraint violations.
*/
@OneToOne(cascade = CascadeType.ALL)private Address address;/**
* A Patron may have several books on loan.
*/
@OneToMany(mappedBy = "patron", cascade = { CascadeType.PERSIST,
CascadeType.MERGE})privateList<Loan> checkedOutResources;/**
* I have zero to many fines. The fines are ordered by the date they were
* added to me.
*/
@OneToMany(cascade = CascadeType.ALL)
@OrderBy("dateAdded")privateList<Fine> fines;public Patron(finalString fName, finalString lName, finalString phone,
final Address a){
setName(newName(fName, lName));
setPhoneNumber(phone);
setAddress(a);}public Address getAddress(){return address;}publicvoid setAddress(Address address){this.address = address;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}publicString getPhoneNumber(){return phoneNumber;}publicvoid setPhoneNumber(String phoneNumber){this.phoneNumber = phoneNumber;}publicList<Loan> getCheckedOutResources(){if(checkedOutResources == null){
checkedOutResources = newArrayList<Loan>();}return checkedOutResources;}publicvoid setCheckedOutResources(List<Loan> checkedOutResources){this.checkedOutResources = checkedOutResources;}publicName getName(){return name;}publicvoid setName(Name name){this.name = name;}publicvoid removeLoan(final Loan loan){
getCheckedOutResources().remove(loan);}publicvoid addLoan(Loan l){
getCheckedOutResources().add(l);}publicList<Fine> getFines(){if(fines == null){
fines = newArrayList<Fine>();}return fines;}publicvoid setFines(List<Fine> fines){this.fines = fines;}publicvoid checkout(finalBook b, finalDate checkoutDate){final Loan l = new Loan(b, this, checkoutDate);
getCheckedOutResources().add(l);
b.setLoan(l);}publicvoid addFine(final Fine f){
getFines().add(f);}publicvoid checkin(final Loan loan){
getCheckedOutResources().remove(loan);}publicdouble calculateTotalFines(){double sum = 0;for(Fine f : getFines()){
sum += f.getAmount();}return sum;}/**
* I clear fines depending on amount tendered. Note that the cascade mode is
* set to all, so if I delete records from my set, they will be removed from
* the database.
*
* @param amountTendered
*
* @return balance after the payment
*/publicdouble pay(finaldouble amountTendered){double totalFines = calculateTotalFines();if(totalFines <= amountTendered){
setFines(newArrayList<Fine>());return amountTendered - totalFines;}else{thrownew InsufficientFunds();}}}
Loan.java
LoanId.java
Book.java
Fine.java
Patron.java